// 版权归Go作者所有。版权所有。
// 此源代码的使用受BSD样式的约束
// 可以在许可证文件中找到的许可证。

package codehost

import (
	"bytes"
	"errors"
	"fmt"
	exec "internal/execabs"
	"io"
	"io/fs"
	"net/url"
	"os"
	"path/filepath"
	"sort"
	"strconv"
	"strings"
	"sync"
	"time"

	"cmd/go/internal/lockedfile"
	"cmd/go/internal/par"
	"cmd/go/internal/web"

	"golang.org/x/mod/semver"
)

// LocalGitRepo与Repo类似，但接受两个Git远程引用
// 以及本地文件系统上存储库的路径。
func LocalGitRepo(remote string) (Repo, error) {
	return newGitRepoCached(remote, true)
}

// notExistError包装另一个错误以保留其原始文本
// 但使其不透明地等同于fs.ErrNotExist。
type notExistError struct {
	err error
}

func (e notExistError) Error() string   { return e.err.Error() }
func (notExistError) Is(err error) bool { return err == fs.ErrNotExist }

const gitWorkDirType = "git3"

var gitRepoCache par.Cache

func newGitRepoCached(remote string, localOK bool) (Repo, error) {
	type key struct {
		remote  string
		localOK bool
	}
	type cached struct {
		repo Repo
		err  error
	}

	c := gitRepoCache.Do(key{remote, localOK}, func() interface{} {
		repo, err := newGitRepo(remote, localOK)
		return cached{repo, err}
	}).(cached)

	return c.repo, c.err
}

func newGitRepo(remote string, localOK bool) (Repo, error) {
	r := &gitRepo{remote: remote}
	if strings.Contains(remote, ":// ") {
		// 这是一条远程路径。
		var err error
		r.dir, r.mu.Path, err = WorkDir(gitWorkDirType, r.remote)
		if err != nil {
			return nil, err
		}

		unlock, err := r.mu.Lock()
		if err != nil {
			return nil, err
		}
		defer unlock()

		if _, err := os.Stat(filepath.Join(r.dir, "objects")); err != nil {
			if _, err := Run(r.dir, "git", "init", "--bare"); err != nil {
				os.RemoveAll(r.dir)
				return nil, err
			}
			// 我们可以说git fetch https：
			// 但是这让我们说git获取origin，这
			// 有点好。更重要的是，使用命名的远程
			// 避免Git LFS出现问题。见golang.org/issue/25605。
			if _, err := Run(r.dir, "git", "remote", "add", "origin", "--", r.remote); err != nil {
				os.RemoveAll(r.dir)
				return nil, err
			}
		}
		r.remoteURL = r.remote
		r.remote = "origin"
	} else {
		// 本地路径。
		// 不允许冒号（不在：
		// 这是rcp风格的host:path语法，有时不是（c:\work）。
		// go命令始终坚持ssh的URL语法。
		if strings.Contains(remote, ":") {
			return nil, fmt.Errorf("git remote cannot use host:path syntax")
		}
		if !localOK {
			return nil, fmt.Errorf("git remote must not be local directory")
		}
		r.local = true
		info, err := os.Stat(remote)
		if err != nil {
			return nil, err
		}
		if !info.IsDir() {
			return nil, fmt.Errorf("%s exists but is not a directory", remote)
		}
		r.dir = remote
		r.mu.Path = r.dir + ".lock"
	}
	return r, nil
}

type gitRepo struct {
	remote, remoteURL string
	local             bool
	dir               string

	mu lockedfile.Mutex // 保护fetchLevel和git repo状态

	fetchLevel int

	statCache par.Cache

	refsOnce sync.Once
	// 参考地图分支和标记参考（例如，“头”、“参考/头/主”）
	// 提交（例如，“37ffd2e798afde829a34e8955b716ab730b2a6d6”）
	refs    map[string]string
	refsErr error

	localTagsOnce sync.Once
	localTags     map[string]bool
}

const (
	// 我们（在此过程中）在git回购中获得了多少资金？
	fetchNone = iota // 还没有
	fetchSome        // 单个散列的浅取
	fetchAll         // “fetch-t origin”：获取所有远程分支和标记
)

// loadLocalTags从本地git缓存加载标记引用
// 进入map r.localTags。
// 应仅称为r.localTagsOnce.Do（r.loadLocalTags）。
func (r *gitRepo) loadLocalTags() {
	// git协议在客户端发送所有已知的REF和ls远程过滤器，
	// 因此，我们不妨在一次拍摄中同时记录头部和标签。
	// 大多数时候我们只关心标签，但有时我们也关心头部。
	out, err := Run(r.dir, "git", "tag", "-l")
	if err != nil {
		return
	}

	r.localTags = make(map[string]bool)
	for _, line := range strings.Split(string(out), "\n") {
		if line != "" {
			r.localTags[line] = true
		}
	}
}

// loadRefs将标头和标记参照从远程加载到地图r.refs中。
// 应仅称为r.refsOnce.Do（r.loadRefs）。
func (r *gitRepo) loadRefs() {
	// git协议在客户端发送所有已知的REF和ls远程过滤器，
	// 因此，我们不妨在一次拍摄中同时记录头部和标签。
	// 大多数时候我们只关心标签，但有时我们也关心头部。
	out, gitErr := Run(r.dir, "git", "ls-remote", "-q", r.remote)
	if gitErr != nil {
		if rerr, ok := gitErr.(*RunError); ok {
			if bytes.Contains(rerr.Stderr, []byte("fatal: could not read Username")) {
				rerr.HelpText = "Confirm the import path was entered correctly.\nIf this is a private repository, see https:// golang.org/doc/faq#git_https获取更多信息。”
			}
		}

		// 如果远程URL根本不存在，理想情况下我们应该处理整个URL
		// 通过将错误包装为NoteExistError，将存储库视为不存在。
		// 对于HTTP和HTTPS，这很容易检测：我们将尝试获取URL
		// 我们自己，看看它服务于什么代码。
		if u, err := url.Parse(r.remoteURL); err == nil && (u.Scheme == "http" || u.Scheme == "https") {
			if _, err := web.GetBytes(u); errors.Is(err, fs.ErrNotExist) {
				gitErr = notExistError{gitErr}
			}
		}

		r.refsErr = gitErr
		return
	}

	r.refs = make(map[string]string)
	for _, line := range strings.Split(string(out), "\n") {
		f := strings.Fields(line)
		if len(f) != 2 {
			continue
		}
		if f[1] == "HEAD" || strings.HasPrefix(f[1], "refs/heads/") || strings.HasPrefix(f[1], "refs/tags/") {
			r.refs[f[1]] = f[0]
		}
	}
	for ref, hash := range r.refs {
		if strings.HasSuffix(ref, "^{}") { // 将未包装的带注释标记记录为标记的值
			r.refs[strings.TrimSuffix(ref, "^{}")] = hash
			delete(r.refs, ref)
		}
	}
}

func (r *gitRepo) Tags(prefix string) ([]string, error) {
	r.refsOnce.Do(r.loadRefs)
	if r.refsErr != nil {
		return nil, r.refsErr
	}

	tags := []string{}
	for ref := range r.refs {
		if !strings.HasPrefix(ref, "refs/tags/") {
			continue
		}
		tag := ref[len("refs/tags/"):]
		if !strings.HasPrefix(tag, prefix) {
			continue
		}
		tags = append(tags, tag)
	}
	sort.Strings(tags)
	return tags, nil
}

func (r *gitRepo) Latest() (*RevInfo, error) {
	r.refsOnce.Do(r.loadRefs)
	if r.refsErr != nil {
		return nil, r.refsErr
	}
	if r.refs["HEAD"] == "" {
		return nil, ErrNoCommits
	}
	return r.Stat(r.refs["HEAD"])
}

// findRef查找给定哈希的某个引用名称，
// 当服务器要求提供ref而不是hash时使用。
// 给定哈希可能有多个ref名称，
// 在这种情况下，它会返回某个名称——不管是哪个名称。
func (r *gitRepo) findRef(hash string) (ref string, ok bool) {
	r.refsOnce.Do(r.loadRefs)
	for ref, h := range r.refs {
		if h == hash {
			return ref, true
		}
	}
	return "", false
}

// minHashDigits是所需的最小位数
// 在接受十六进制数字序列作为潜在标识之前
// git回购中的特定提交。（当然，用户总是可以
// 指定更多数字，许多数字将粘贴在所有40个数字中，
// 但是git的许多命令默认为打印短哈希
// 作为7位数字。）
const minHashDigits = 7

// 统计本地存储库中的给定版本，
// 或者它从远程存储库获取更多信息，然后重试。
func (r *gitRepo) stat(rev string) (*RevInfo, error) {
	if r.local {
		return r.statLocal(rev, rev)
	}

	// 快速路径：也许rev是我们本地已经有的一个散列。
	didStatLocal := false
	if len(rev) >= minHashDigits && len(rev) <= 40 && AllHex(rev) {
		if info, err := r.statLocal(rev, rev); err == nil {
			return info, nil
		}
		didStatLocal = true
	}

	// 也许rev是我们本地已有的标签。
	// （请注意，我们排除了可能过时的分支。）
	r.localTagsOnce.Do(r.loadLocalTags)
	if r.localTags[rev] {
		return r.statLocal(rev, "refs/tags/"+rev)
	}

	// 可能rev是远程服务器上的标记或分支的名称。
	// 或者它可能是命名ref的哈希的前缀。
	// 尝试解析为ref（git名称）和full（40个十六进制数字）提交散列。
	r.refsOnce.Do(r.loadRefs)
	// 如果git失败，loadRefs可能会返回错误，例如segfaults或
	// 无法加载私有回购，但将检查延迟到else块
	// 下面，以防本地缓存中已经存在有问题的版本。
	var ref, hash string
	if r.refs["refs/tags/"+rev] != "" {
		ref = "refs/tags/" + rev
		hash = r.refs[ref]
		// 保持原样：假定标签不会改变含义。
	} else if r.refs["refs/heads/"+rev] != "" {
		ref = "refs/heads/" + rev
		hash = r.refs[ref]
		rev = hash // 替换rev，因为REF/heads/foo的含义可能会更改。
	} else if rev == "HEAD" && r.refs["HEAD"] != "" {
		ref = "HEAD"
		hash = r.refs[ref]
		rev = hash // 更换rev，因为头部的含义可能会改变。
	} else if len(rev) >= minHashDigits && len(rev) <= 40 && AllHex(rev) {
		// 至少，我们有一个哈希前缀，可以在下面的获取之后查找。
		// 也许我们可以使用已知的引用将其映射到一个完整的散列。
		prefix := rev
		// 检查rev是否为已知ref散列的前缀。
		for k, h := range r.refs {
			if strings.HasPrefix(h, prefix) {
				if hash != "" && hash != h {
					// 哈希是一个不明确的哈希前缀。
					// 更多的信息不会改变这一点。
					return nil, fmt.Errorf("ambiguous revision %s", rev)
				}
				if ref == "" || ref > k { // 当多个引用指向同一散列时，决定性地断开连接。
					ref = k
				}
				rev = h
				hash = h
			}
		}
		if hash == "" && len(rev) == 40 { // 未找到ref，但rev是一个完整哈希。
			hash = rev
		}
	} else {
		if r.refsErr != nil {
			return nil, r.refsErr
		}
		return nil, &UnknownRevisionError{Rev: rev}
	}

	// 保护r.fetchLevel和“fetch more and more”序列。
	unlock, err := r.mu.Lock()
	if err != nil {
		return nil, err
	}
	defer unlock()

	// 也许r.localTags在加载本地标记时没有ref，
	// 但我们已经做了回迁，把我们需要的哈希值拉下来
	// （或者已经有了我们需要的哈希，只是没有它的标签）。
	// 无论哪种方式，请在返回网络I/O之前尝试本地统计。
	if !didStatLocal {
		if info, err := r.statLocal(rev, hash); err == nil {
			if strings.HasPrefix(ref, "refs/tags/") {
				// 确保标记存在，以便下次运行go命令时它将位于localTags中。
				Run(r.dir, "git", "tag", strings.TrimPrefix(ref, "refs/tags/"), hash)
			}
			return info, nil
		}
	}

	// 如果我们知道我们需要的一个特定的提交和它的ref，获取它。
	// 我们不获取任意散列（当我们不知道ref时）
	// 因为我们希望避免导入一个不是
	// 可从refs/tags/*或refs/heads/*或HEAD访问。
	// Gerrit和GitHub都将每个CL/PR公开为命名引用，
	// 我们不希望这些承诺伪装成真实的
	// 主回购协议中的伪版本。
	if r.fetchLevel <= fetchSome && ref != "" && hash != "" && !r.local {
		r.fetchLevel = fetchSome
		var refspec string
		if ref != "" && ref != "HEAD" {
			// 如果我们确实知道ref名称，请在本地保存映射
			// 因此（如果它是一个标记），它可以显示在localTags中
			// 在未来的通话中。此外，一些服务器拒绝允许
			// ref规范中的完整哈希，因此如果已知，则首选ref名称。
			refspec = ref + ":" + ref
		} else {
			// 获取散列，但给它一个本地名称（refs/dummy），
			// 因为这会触发创建任何
			// 哈希的其他已知远程标记。我们从不使用
			// 参考/虚拟（不是参考/标记/虚拟），它将
			// 在下一个命令中覆盖，这很好。
			ref = hash
			refspec = hash + ":refs/dummy"
		}
		_, err := Run(r.dir, "git", "fetch", "-f", "--depth=1", r.remote, refspec)
		if err == nil {
			return r.statLocal(rev, ref)
		}
		// 不要试图聪明地解析错误。
		// 这太复杂了，git版本的差异太大了。
		// 不管出了什么问题，都要重新开始。
	}

	// 终极手段
	// 取下所有的头和标签，希望我们想要的哈希在历史记录中。
	if err := r.fetchRefsLocked(); err != nil {
		return nil, err
	}

	return r.statLocal(rev, rev)
}

// fetchRefsLocked从原点获取所有标头和标记，以及
// 这些罪行的祖先。
// None
// 我们只获取头和标记，而不是任意的其他提交：我们不想这样做
// 拉入分支外提交（例如被拒绝的GitHub拉入请求），该请求
// 服务器可能愿意提供。（请参阅stat方法中的注释。）
// 如需了解更多详细信息。）
// None
// fetchRefsLocked要求r.mu在调用期间保持锁定。
func (r *gitRepo) fetchRefsLocked() error {
	if r.fetchLevel < fetchAll {
		// 注意：要解决影响Git客户端的bug，至少要达到2.23.0
		// （2019-08-16），我们必须首先扩展本地参考集合，然后
		// 不允许将存储库作为单独的获取操作。（见
		// golang.org/issue/34266和
		// https:

		if _, err := Run(r.dir, "git", "fetch", "-f", r.remote, "refs/heads/*:refs/heads/*", "refs/tags/*:refs/tags/*"); err != nil {
			return err
		}

		if _, err := os.Stat(filepath.Join(r.dir, "shallow")); err == nil {
			if _, err := Run(r.dir, "git", "fetch", "--unshallow", "-f", r.remote); err != nil {
				return err
			}
		}

		r.fetchLevel = fetchAll
	}
	return nil
}

// statLocal返回一个RevInfo，描述本地git存储库中的rev。
// 它使用版本作为info.version。
func (r *gitRepo) statLocal(version, rev string) (*RevInfo, error) {
	out, err := Run(r.dir, "git", "-c", "log.showsignature=false", "log", "-n1", "--format=format:%H %ct %D", rev, "--")
	if err != nil {
		return nil, &UnknownRevisionError{Rev: rev}
	}
	f := strings.Fields(string(out))
	if len(f) < 2 {
		return nil, fmt.Errorf("unexpected response from git log: %q", out)
	}
	hash := f[0]
	if strings.HasPrefix(hash, version) {
		version = hash // 扩展到完全散列
	}
	t, err := strconv.ParseInt(f[1], 10, 64)
	if err != nil {
		return nil, fmt.Errorf("invalid time from git log: %q", out)
	}

	info := &RevInfo{
		Name:    hash,
		Short:   ShortenSHA1(hash),
		Time:    time.Unix(t, 0).UTC(),
		Version: hash,
	}

	// 添加标签。输出如下所示：
	// ede458df7cd0fdca520df19a33158086a8a68e81 1523994202主机->标签：v1.2.4-注释，标签：v1.2.3，原点/主机，原点/主机
	for i := 2; i < len(f); i++ {
		if f[i] == "tag:" {
			i++
			if i < len(f) {
				info.Tags = append(info.Tags, strings.TrimSuffix(f[i], ","))
			}
		}
	}
	sort.Strings(info.Tags)

	// 使用散列作为上面的信息版本。
	// 如果出现在标记列表中，请使用调用者的建议版本
	// （过滤掉分支名称、头）。
	for _, tag := range info.Tags {
		if version == tag {
			info.Version = version
		}
	}

	return info, nil
}

func (r *gitRepo) Stat(rev string) (*RevInfo, error) {
	if rev == "latest" {
		return r.Latest()
	}
	type cached struct {
		info *RevInfo
		err  error
	}
	c := r.statCache.Do(rev, func() interface{} {
		info, err := r.stat(rev)
		return cached{info, err}
	}).(cached)
	return c.info, c.err
}

func (r *gitRepo) ReadFile(rev, file string, maxSize int64) ([]byte, error) {
	// TODO:可以使用git cat文件—批处理。
	info, err := r.Stat(rev) // 将rev下载到本地git回购
	if err != nil {
		return nil, err
	}
	out, err := Run(r.dir, "git", "cat-file", "blob", info.Name+":"+file)
	if err != nil {
		return nil, fs.ErrNotExist
	}
	return out, nil
}

func (r *gitRepo) ReadFileRevs(revs []string, file string, maxSize int64) (map[string]*FileRev, error) {
	// 创建用于保存结果的空间。
	files := make(map[string]*FileRev)
	for _, rev := range revs {
		f := &FileRev{Rev: rev}
		files[rev] = f
	}

	// 收集当地已知的转速。
	need, err := r.readFileRevs(revs, file, files)
	if err != nil {
		return nil, err
	}
	if len(need) == 0 {
		return files, nil
	}

	// 生成可能有帮助的已知远程引用的列表。
	var redo []string
	r.refsOnce.Do(r.loadRefs)
	if r.refsErr != nil {
		return nil, r.refsErr
	}
	for _, tag := range need {
		if r.refs["refs/tags/"+tag] != "" {
			redo = append(redo, tag)
		}
	}
	if len(redo) == 0 {
		return files, nil
	}

	// 保护r.fetchLevel和“fetch more and more”序列。
	// 参见上面的统计方法。
	unlock, err := r.mu.Lock()
	if err != nil {
		return nil, err
	}
	defer unlock()

	if err := r.fetchRefsLocked(); err != nil {
		return nil, err
	}

	if _, err := r.readFileRevs(redo, file, files); err != nil {
		return nil, err
	}

	return files, nil
}

func (r *gitRepo) readFileRevs(tags []string, file string, fileMap map[string]*FileRev) (missing []string, err error) {
	var stdin bytes.Buffer
	for _, tag := range tags {
		fmt.Fprintf(&stdin, "refs/tags/%s\n", tag)
		fmt.Fprintf(&stdin, "refs/tags/%s:%s\n", tag, file)
	}

	data, err := RunWithStdin(r.dir, &stdin, "git", "cat-file", "--batch")
	if err != nil {
		return nil, err
	}

	next := func() (typ string, body []byte, ok bool) {
		var line string
		i := bytes.IndexByte(data, '\n')
		if i < 0 {
			return "", nil, false
		}
		line, data = string(bytes.TrimSpace(data[:i])), data[i+1:]
		if strings.HasSuffix(line, " missing") {
			return "missing", nil, true
		}
		f := strings.Fields(line)
		if len(f) != 3 {
			return "", nil, false
		}
		n, err := strconv.Atoi(f[2])
		if err != nil || n > len(data) {
			return "", nil, false
		}
		body, data = data[:n], data[n:]
		if len(data) > 0 && data[0] == '\r' {
			data = data[1:]
		}
		if len(data) > 0 && data[0] == '\n' {
			data = data[1:]
		}
		return f[1], body, true
	}

	badGit := func() ([]string, error) {
		return nil, fmt.Errorf("malformed output from git cat-file --batch")
	}

	for _, tag := range tags {
		commitType, _, ok := next()
		if !ok {
			return badGit()
		}
		fileType, fileData, ok := next()
		if !ok {
			return badGit()
		}
		f := fileMap[tag]
		f.Data = nil
		f.Err = nil
		switch commitType {
		default:
			f.Err = fmt.Errorf("unexpected non-commit type %q for rev %s", commitType, tag)

		case "missing":
			// 注意：f.Err不能满足os.IsNotExist。这是为有效提交中不存在的文件保留的。
			f.Err = fmt.Errorf("no such rev %s", tag)
			missing = append(missing, tag)

		case "tag", "commit":
			switch fileType {
			default:
				f.Err = &fs.PathError{Path: tag + ":" + file, Op: "read", Err: fmt.Errorf("unexpected non-blob type %q", fileType)}
			case "missing":
				f.Err = &fs.PathError{Path: tag + ":" + file, Op: "read", Err: fs.ErrNotExist}
			case "blob":
				f.Data = fileData
			}
		}
	}
	if len(bytes.TrimSpace(data)) != 0 {
		return badGit()
	}

	return missing, nil
}

func (r *gitRepo) RecentTag(rev, prefix string, allowed func(string) bool) (tag string, err error) {
	info, err := r.Stat(rev)
	if err != nil {
		return "", err
	}
	rev = info.Name // 扩展哈希前缀

	// 描述使用“git for each ref”设置标记和错误，并报告
	// 结果是确定的。
	describe := func() (definitive bool) {
		var out []byte
		out, err = Run(r.dir, "git", "for-each-ref", "--format", "%(refname)", "refs/tags", "--merged", rev)
		if err != nil {
			return true
		}

		// 带前缀的标记不是有效的semver标记，因此比较时不带前缀，而只比较带正确前缀的标记
		var highest string
		for _, line := range strings.Split(string(out), "\n") {
			line = strings.TrimSpace(line)
			// git对每种ref格式都支持lstrip，但它是在v2.13.0中添加的。在这里脱衣
			// 而是支持GitV2.7.0。
			if !strings.HasPrefix(line, "refs/tags/") {
				continue
			}
			line = line[len("refs/tags/"):]

			if !strings.HasPrefix(line, prefix) {
				continue
			}

			semtag := line[len(prefix):]
			// 只考虑有效和完整的标签（不只是主要的小前缀）。
			// 注意：不要替换对semver的调用。请与semver.Max进行比较。
			// 我们想返回实际的标记，而不是它的规范化版本，
			// 而semver.Max目前正在规范化（见golang.org/issue/32700）。
			if c := semver.Canonical(semtag); c == "" || !strings.HasPrefix(semtag, c) || !allowed(semtag) {
				continue
			}
			if semver.Compare(semtag, highest) > 0 {
				highest = semtag
			}
		}

		if highest != "" {
			tag = prefix + highest
		}

		return tag != "" && !AllHex(tag)
	}

	if describe() {
		return tag, err
	}

	// Git在请求的版本之前未找到版本标记。
	// 查看是否存在任何合理的标记。
	tags, err := r.Tags(prefix + "v")
	if err != nil {
		return "", err
	}
	if len(tags) == 0 {
		return "", nil
	}

	// 有一些看似合理的标签，但我们不知道rev是否是它们的后代。
	// 获取历史记录以了解情况。

	unlock, err := r.mu.Lock()
	if err != nil {
		return "", err
	}
	defer unlock()

	if err := r.fetchRefsLocked(); err != nil {
		return "", err
	}

	// 如果我们已经达到了这一点，我们就拥有了所有可以访问的提交
	// 从所有的头和标签。
	// None
	// 我们应该缺少的唯一参考是那些再也找不到的参考
	// （或从任何分支或标签（包括主分支或标签）无法访问
	// 分支，我们无论如何都不想解决它们（它们可能是
	// 由于某种原因而无法到达）。
	// None
	// 最后再试一次，以防我们在一起的时候，其他goroutine抓到了rev
	// 等待锁。
	describe()
	return tag, err
}

func (r *gitRepo) DescendsFrom(rev, tag string) (bool, error) {
	// 在版本1.8.0中，“--is祖先”标志被添加到“git合并基础”中，因此
	// 这不适用于Git1.7.1。根据golang.org/issue/28550，cmd/go
	// Git1.7.1已经不起作用了，所以至少它不是一个回归。
	// None
	// git merge base——如果rev是祖先，则是状态为0的祖先退出，或者
	// 1如果没有。
	_, err := Run(r.dir, "git", "merge-base", "--is-ancestor", "--", tag, rev)

	// Git报告退出代码为0的“是祖先”，报告退出代码为0的“不是祖先”
	// 退出代码1。
	// 不幸的是，如果我们已经取得了一个浅历史的版本，git
	// 已观察到merge base报告为假阴性，因此暂时不要停止
	// 即使退出代码是1！
	if err == nil {
		return true, nil
	}

	// 查看标签和版本是否存在。
	tags, err := r.Tags(tag)
	if err != nil {
		return false, err
	}
	if len(tags) == 0 {
		return false, nil
	}

	// 注意：r.stat非常小心地不获取我们不应该知道的提交
	// 大约，就像被拒绝的GitHub拉请求一样，所以不要尝试短路
	// 在这里。
	if _, err = r.stat(rev); err != nil {
		return false, err
	}

	// 现在获取历史，以便git可以搜索路径。
	unlock, err := r.mu.Lock()
	if err != nil {
		return false, err
	}
	defer unlock()

	if r.fetchLevel < fetchAll {
		// 获取所有引用和标头的完整历史记录。那会更多
		// 只从rev到tag获取历史记录非常有效，但这要多得多
		// 复杂，任何类型的浅层提取都很可能触发
		// JGit服务器和/或go命令中的错误。
		if err := r.fetchRefsLocked(); err != nil {
			return false, err
		}
	}

	_, err = Run(r.dir, "git", "merge-base", "--is-ancestor", "--", tag, rev)
	if err == nil {
		return true, nil
	}
	if ee, ok := err.(*RunError).Err.(*exec.ExitError); ok && ee.ExitCode() == 1 {
		return false, nil
	}
	return false, err
}

func (r *gitRepo) ReadZip(rev, subdir string, maxSize int64) (zip io.ReadCloser, err error) {
	// TODO:使用maxSize或删除它。
	args := []string{}
	if subdir != "" {
		args = append(args, "--", subdir)
	}
	info, err := r.Stat(rev) // 将rev下载到本地git回购
	if err != nil {
		return nil, err
	}

	unlock, err := r.mu.Lock()
	if err != nil {
		return nil, err
	}
	defer unlock()

	if err := ensureGitAttributes(r.dir); err != nil {
		return nil, err
	}

	// 令人难以置信的是，git根据
	// 它是否在Windows系统上运行，以尝试正常化
	// 文本文件行结尾。设置-c core.autocrlf=仅输入意味着
	// 将文件在途中转换为回购协议，而不是在途中（归档）。
	// -c core.eol=lf应该是不必要的，但还是要设置它。
	archive, err := Run(r.dir, "git", "-c", "core.autocrlf=input", "-c", "core.eol=lf", "archive", "--format=zip", "--prefix=prefix/", info.Name, args)
	if err != nil {
		if bytes.Contains(err.(*RunError).Stderr, []byte("did not match any files")) {
			return nil, fs.ErrNotExist
		}
		return nil, err
	}

	return io.NopCloser(bytes.NewReader(archive)), nil
}

// EnsureRegitatAttributes确保导出子对象和导出忽略功能
// 此回购已禁用。这将在运行git之前运行
// 存档，以便生成产生一致Ziphes的zip文件
// 对于给定的修订版，独立于git版本和
// 回购协议的规模。
// None
// 请参阅：https:
func ensureGitAttributes(repoDir string) (err error) {
	const attr = "\n* -export-subst -export-ignore\n"

	d := repoDir + "/info"
	p := d + "/attributes"

	if err := os.MkdirAll(d, 0755); err != nil {
		return err
	}

	f, err := os.OpenFile(p, os.O_CREATE|os.O_APPEND|os.O_RDWR, 0666)
	if err != nil {
		return err
	}
	defer func() {
		closeErr := f.Close()
		if closeErr != nil {
			err = closeErr
		}
	}()

	b, err := io.ReadAll(f)
	if err != nil {
		return err
	}
	if !bytes.HasSuffix(b, []byte(attr)) {
		_, err := f.WriteString(attr)
		return err
	}

	return nil
}
